package com.settlement.system.controller;

import com.settlement.system.csv.*;
import com.settlement.system.common.enums.ExcelFileType;
import com.settlement.system.common.enums.ReturnStatusEnum;
import com.settlement.system.common.exception.ServiceException;
import com.settlement.system.funciton.Function;
import com.settlement.system.model.entity.common.BaseModel;
import com.settlement.system.model.entity.common.TblCustomizedExporeDtl;
import com.settlement.system.reply.ObjectDataResponse;
import com.settlement.system.service.ITblCustomizedExporeMainService;
import com.settlement.system.common.util.common.BeanValidator;
import com.settlement.system.common.util.common.DateUtil;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.read.builder.ExcelReaderBuilder;
import com.alibaba.excel.write.builder.ExcelWriterBuilder;
import com.alibaba.excel.write.handler.AbstractRowWriteHandler;
import com.alibaba.excel.write.metadata.WriteSheet;

import com.settlement.system.service.csv.DateSpiltCsvDatas;
import io.swagger.annotations.ApiOperation;
import jakarta.validation.ConstraintViolation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.beanutils.BeanMap;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.ResultContext;
import org.apache.ibatis.session.ResultHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;

import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @author
 */
@Slf4j
public abstract class ExcelController<ModelType extends BaseModel> extends BaseController<ModelType> {

    @Value("${system.export-root.path}")
    private String exportRootPath;

    protected ThreadLocal<String> message = ThreadLocal.withInitial(String::new);

    protected ThreadLocal<Boolean> needCheck = ThreadLocal.withInitial(()->false);

    @Resource
    private ITblCustomizedExporeMainService customizedExporeMainService;

    private final ExecutorService executors = Executors.newFixedThreadPool(8);


    @ApiOperation("导入模板")
    @PostMapping("import")
    public ObjectDataResponse importFile(MultipartFile file, HttpServletRequest request) {
        String needCheck = request.getParameter("needCheck");
        this.needCheck.set("true".equals(needCheck));
        String errorMessage = null;
        List<String> errorMessageList = null;
        try {
            message = ThreadLocal.withInitial(String::new);
            ImportDataListener listener = new ImportDataListener();
            ExcelReaderBuilder builder = EasyExcel.read(file.getInputStream(), getModelClass(), listener);
            builder.doReadAll();
            errorMessage = listener.getErrorMessage();
                if (listener.rowCount == 0) {
                errorMessage = "导入内容为空";
            }
        } catch (Exception ex) {
            log.error("导入转化出错", ex);
            errorMessage = "系统异常,请联系管理员";

        }

        if (StringUtils.isEmpty(errorMessage)) {
            if(StringUtils.isNotEmpty(this.message.get())){
                return new ObjectDataResponse(this.message.get(),ReturnStatusEnum.REQUEST_SUCCESS.getValue(), this.message.get());
            }
            return new ObjectDataResponse(ReturnStatusEnum.IMPORT_SUCCESS.getValue(), "导入成功");
        }
        errorMessageList = new ArrayList<>();
        errorMessageList.add(errorMessage);
        return new ObjectDataResponse<List>(errorMessageList, ReturnStatusEnum.IMPORT_ERROR.getValue(), errorMessage);
    }

    /**
     * 返回实体类
     * @return
     */
    public abstract Class<?> getModelClass();

    public AbstractRowWriteHandler getCommentWriterHandler() {
        return null;
    }

    public String getSheetName() {
        return null;
    }

    ;

    /**
     * 导入数据的监听器
     */
    public class ImportDataListener extends AnalysisEventListener<ModelType> {

        public String errorMessage = null;
        public List<ModelType> data = new ArrayList<>();
        /**
         * 总行数
         */
        public int rowCount = 0;
        /**
         * 最大行数限制
         */
        final int MAX_ROWS = 50*10000;
        @Override
        public void invoke(ModelType modelType, AnalysisContext analysisContext) {
            modelType = clearBeanStringBlank(modelType);
            if(isEmpty(modelType)){
                return;
            }
            errorMessage = verifyImportObject(modelType, analysisContext);
            if (!StringUtils.isEmpty(errorMessage)) {
                errorMessage = "第" + (data.size() + 2) + "行数据" + errorMessage;
            }
            setDefualt(modelType);
            data.add(modelType);
            rowCount++;
            if(rowCount>MAX_ROWS){
                errorMessage = "总行数不能大于"+MAX_ROWS;
            }
        }

        @Override
        public void doAfterAllAnalysed(AnalysisContext analysisContext) {
            try {
                if(StringUtils.isNotEmpty(errorMessage)){
                    return;
                }
                errorMessage = validateAllDate(data,analysisContext);
                if(StringUtils.isEmpty(errorMessage)){
                    saveData(data,analysisContext);
                }
                data.clear();
            } catch (ServiceException e) {
                log.error("保存失败", e);

                if (StringUtils.isEmpty(e.getMessage())) {
                    errorMessage = "导入异常，请联系管理管理员";
                } else {
                    errorMessage = e.getMessage();
                }

            }
        }

        @Override
        public boolean hasNext(AnalysisContext context) {
            if (StringUtils.isEmpty(errorMessage)) {
                return true;
            }
            return false;
        }

        public String getErrorMessage() {
            return errorMessage;
        }
    }


    /**
     * 校验所有数据是否达到预期
     * @param data
     */
    protected String validateAllDate(List<ModelType> data){
        return null;
    }

    protected String validateAllDate(List<ModelType> data, AnalysisContext analysisContext){
        return validateAllDate(data);
    }

    /**
     * 验证数据
     *
     * @param modelType
     * @return
     */
    public String verifyImportObject(ModelType modelType, AnalysisContext analysisContext) {
        List<String> error = new ArrayList<>();
        ConstraintViolation validate = BeanValidator.validate(modelType);
        if (validate != null) {
            return validate.getMessage();
        }
        return null;
    }

    /**
     * 保存数据
     *
     * @param datas
     * @throws ServiceException
     */
    protected void saveData(List<ModelType> datas) throws ServiceException {
        baseService.save(datas);
    }

    protected void saveData(List<ModelType> datas, AnalysisContext analysisContext) throws ServiceException {
        saveData(datas);
    }

    /**
     * 通用的导出
     *
     * @param request
     * @param response
     */
    @GetMapping("export")
    @ApiOperation("通用导出")
    public void exportExcel(HttpServletRequest request, HttpServletResponse response) throws Exception {
        try {
            export(request, response, this::getExportDate);
        }catch (Exception ex){

            throw ex;
        }

    }

    @PostMapping("exportPost")
    @ApiOperation("通用导出")
    public void exportExcelPost(HttpServletRequest request, HttpServletResponse response,@RequestBody Map<String, Object> params) throws Exception {
        try {
            export(request, response, this::getExportDate,params);
        }catch (Exception ex){

            throw ex;
        }

    }

    /**
     * 通用的导出
     *
     * @param request
     * @param response
     */
    @GetMapping("exportExcel")
    @ApiOperation("通用导出Excel到zip")
    public void exportExcelZip(HttpServletRequest request, HttpServletResponse response) throws Exception {
        exportExcelToZip(request, response);
    }

    protected void export(HttpServletRequest request, HttpServletResponse response, Function<Map<String, Object>, List<ModelType>> getDatas) throws IOException, ServiceException {
        export(request,response,getDatas,null);
    }

    protected void export(HttpServletRequest request, HttpServletResponse response, Function<Map<String, Object>, List<ModelType>> getDatas,Map<String,Object> params) throws IOException, ServiceException {
        if(params==null){
            params = buildParam(request);
        }
        setParamsModel(params);
        Object fileName = params.containsKey("fileName") ? params.get("fileName").toString() : System.currentTimeMillis();
        File file = new File(exportRootPath + fileName + ".xlsx");

        File dir = new File(exportRootPath);
        if(!dir.exists()){
            dir.mkdirs();
        }

        FileInputStream in = null;
        OutputStream out = null;

        try {
            List<ModelType> data = getDatas.apply(params);
            ExcelWriterBuilder writerBuilder = EasyExcel
                    .write(file, getModelClass())
                    .registerWriteHandler(getCommentWriterHandler());
            if (getCommentWriterHandler() != null) {
                writerBuilder = writerBuilder.inMemory(Boolean.TRUE);
            }
            writerBuilder.sheet(getSheetName()).doWrite(data);

            response.reset();
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/x-msdownload");
            response.setHeader("Content-Disposition", "attachment;filename=" + file.getName());

            in = new FileInputStream(file);
            out = response.getOutputStream();
            byte[] buf = new byte[4089];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }

        } catch (Exception e) {
            log.error("导出异常", e);
        } finally {
            if (null != in) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (null != out) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            file.delete();
        }
    }

    protected List<ModelType> getExportDate(Map<String, Object> params) throws ServiceException {
        try{
            return baseService.selectByParam(params);
        }catch (Exception ex){
            ex.printStackTrace();
        }
        return new ArrayList<>();
    }

    @GetMapping("customizedExport")
    @ApiOperation("自定义导出")
    public void customizedExportExcel(
            @RequestParam("menuName") String menuName,
            @RequestParam("customizedExporeName") String customizedExporeName,
            HttpServletRequest request, HttpServletResponse response) throws Exception {
        Map<String, Object> params = buildParam(request);
        setParamsModel(params);
        //英文的圆括号被拦截转义，需要再转回来
        // customizedExporeName = StringEscapeUtils.unescapeHtml(customizedExporeName);
        try {
            List<TblCustomizedExporeDtl> exporeDtls = customizedExporeMainService.getByBothName(menuName, customizedExporeName);

            String path = exportRootPath + System.currentTimeMillis() + File.separator;
            String fileType = (String) params.get("fileType");
            if(StringUtils.isEmpty(fileType)){
                //默认是CSV格式，原来的代码不用改
                fileType = ExcelFileType.CSV.toString();
            }
            CsvExportUtil.createCsv()
                    //这个是给自定义导出XLSX时，反射封装数据时用的
                    .headClass(getModelClass())
                    .headers(getCustomizedHeaders(exporeDtls))
                    .customized(true)
                    .fileName(customizedExporeName)
                    .fileType(ExcelFileType.getByName(fileType))
                    .model(getCsvModel(params))
                    .parentPath(path)
                    .csvDatas(getCsvDatas(params))
                    .export(request,response);
        }catch (Exception ex){

            throw ex;
        }

    }


    @PostMapping("customizedPostExport")
    @ApiOperation("自定义导出")
    public void customizedPostExportExcel(
            HttpServletRequest request, HttpServletResponse response,@RequestBody Map<String, Object> params) throws Exception {
        setParamsModel(params);
        //英文的圆括号被拦截转义，需要再转回来
        String menuName = (String)params.get("menuName");
        String customizedExporeName = (String) params.get("customizedExporeName");
        // customizedExporeName = StringEscapeUtils.unescapeHtml(customizedExporeName);
        try {
            List<TblCustomizedExporeDtl> exporeDtls = customizedExporeMainService.getByBothName(menuName, customizedExporeName);

            String path = exportRootPath + System.currentTimeMillis() + File.separator;
            String fileType = (String) params.get("fileType");
            if(StringUtils.isEmpty(fileType)){
                //默认是CSV格式，原来的代码不用改
                fileType = ExcelFileType.CSV.toString();
            }
            CsvExportUtil.createCsv()
                    //这个是给自定义导出XLSX时，反射封装数据时用的
                    .headClass(getModelClass())
                    .headers(getCustomizedHeaders(exporeDtls))
                    .customized(true)
                    .fileName(customizedExporeName)
                    .fileType(ExcelFileType.getByName(fileType))
                    .model(getCsvModel(params))
                    .parentPath(path)
                    .csvDatas(getCsvDatas(params))
                    .export(request,response);
        }catch (Exception e){

            throw e;
        }

    }

    protected Integer getCsvModel(Map<String, Object> params) {
        return CsvExportBuilder.MULTI_ZIP_MODEL; // 默认模式
    }

    protected CsvDatas getCsvDatas(Map<String, Object> params) throws Exception{
        return new DateSpiltCsvDatas(baseService, params);
    }

    /**
     * 获取自定义导出的头部
     *
     * @param exporeDtls
     * @return
     */
    private Headers getCustomizedHeaders(List<TblCustomizedExporeDtl> exporeDtls) {
        Headers headers = HeadParseUtil.parse(getModelClass());
        if (CollectionUtils.isEmpty(exporeDtls)) {
            return headers;
        }

        List<Header> currentList = new ArrayList<>();
        List<Header> headerList = headers.getHeaders();
        for (TblCustomizedExporeDtl exporeDtl : exporeDtls) {
            Header header = new Header();
            header.setTitle(exporeDtl.getFieldDescription());
            header.setField(exporeDtl.getFieldName());
            header.setOrder(exporeDtl.getPriority());
            boolean exists = false;
            for (Header info : headerList) {
                if(header.getField().equals(info.getField())){
                    exists = true;
                }
                if (header.getField().equals(info.getField()) && info.getConverter() != null) {
                    header.setConverter(info.getConverter());
                }
            }
            if(exists){
                currentList.add(header);
                exists = false;
            }
        }
        return new Headers(currentList);
    }

    /**
     * 把对象转化为map
     *
     * @param object
     * @return
     */
    protected Map<String, Object> objectToMap(Object object) {
        Map<String, Object> params = new BeanMap(object);
        return params;
    }

    /**
     * 空对象检查，避免“EXCEL会把空行当作一行，误报非空错误”的情况
     * @param modelType
     * @return
     */
    protected boolean isEmpty(ModelType modelType) {
        if (modelType == null) {
            return true;
        }
        Map<String, Object> map = new HashMap<>(16);
        map.putAll(objectToMap(modelType));
        map.remove("class");
        Set<Map.Entry<String, Object>> entries = map.entrySet();

        for(Map.Entry<String, Object> entry : entries){
            if(entry.getValue() !=null){
                return false;
            }
        }
        return true;
    }

    /**
     * 读取数据库
     * @author Ximi
     *
     */
    public class GetTask<T> implements Callable<Boolean> {

        private AtomicBoolean hasException;
        public Map<String,Object> params;
        public LinkedBlockingQueue<T> queue;
        private Exception exception = null;

        public GetTask(Map<String, Object> params,
                       LinkedBlockingQueue<T> queue,AtomicBoolean hasException
                      ) {
            this.params = (Map<String, Object>) ((HashMap)params).clone();
            this.queue = queue;
            this.hasException = hasException;
        }

        @Override
        public Boolean call() throws Exception {
            baseService.getPageDataByHandler(params,new ResultHandler<T>() {
            //sendToReturnManager.getAllList(params, new ResultHandler<T>() {

                @Override
                public void handleResult(ResultContext<? extends T> resultContext) {
                    try {
                        queue.put(resultContext.getResultObject());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        hasException.getAndSet(true);
                    }
                }
            });

            return true;
        }
    }

    class ExcelExportComplex{
        private String fileName;
        private String sheetName;

        private ExcelWriterBuilder writerBuilder;

        private int rowCount;
        private int maxSize;

        public void setRowCount(int rowCount) {
            this.rowCount = rowCount;
        }

        private void addRow(List data){
            System.out.println(sheetName);
            writerBuilder.sheet(sheetName).doWrite(data);
        }

        public ExcelExportComplex(String fileName, String sheetName, ExcelWriterBuilder writerBuilder, int maxSize) {
            this.fileName = fileName;
            this.sheetName = sheetName;
            this.writerBuilder = writerBuilder;
            this.maxSize = maxSize;
            this.rowCount = 0;
        }

        public int getRowCount() {
            return rowCount;
        }

        public int getMaxSize() {
            return maxSize;
        }
    }



    /**
     * 保存到excel文件的任务
     * @author Ximi
     *
     */

    public class SaveTask<T> extends Thread implements Runnable{

        private AtomicBoolean hasException;
        private Boolean isEnd = false;
        private LinkedBlockingQueue<T> queue;
        private ExcelExportComplex exportComplex;


        public SaveTask(LinkedBlockingQueue<T> queue, ExcelExportComplex exportComplex,AtomicBoolean hasException) {
            this.queue = queue;
            this.exportComplex = exportComplex;
            this.hasException = hasException;
        }

        @Override
        public void run() {
            int rowNum = 0;
            List<T> data = new ArrayList<>();
            while((queue.size() != 0 || !isEnd) &&
                    exportComplex.getRowCount() < exportComplex.getMaxSize() &&
                hasException.get() == false
            ){
                try {
                    boolean flag = (exportComplex.getRowCount()<exportComplex.getMaxSize()) && (queue.size() != 0 || !isEnd);
                    if(flag){
                        T t = queue.poll(100, TimeUnit.MILLISECONDS);
                        if(t != null){
                            data.add(t);
                            exportComplex.setRowCount(exportComplex.getRowCount()+1);
                        }
                    }
                    boolean flag2 = (queue.size() == 0 && isEnd) || exportComplex.getRowCount() == exportComplex.getMaxSize();
                    if(flag2){
                        exportComplex.addRow(data);
                        System.out.println(exportComplex.sheetName+" 退出");
                        break;
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    hasException.getAndSet(true);
                }
                if(exportComplex.getRowCount()%10000==0){
                    System.out.println(exportComplex.sheetName+" "+DateUtil.getCurrentDateTimeToStr2());
                }
            }
        }
    }

    protected<T> void exportExcelToZip(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Date startDate = new Date();
        Map<String, Object> params = buildParam(request);
        setParamsModel(params);
        String fileName = params.containsKey("fileName") ? params.get("fileName").toString() : System.currentTimeMillis()+"";
        final String dot = ".";
        if(fileName.indexOf(dot)!=-1){
            fileName = fileName.substring(0,fileName.indexOf("."));
        }

        String path = exportRootPath + System.currentTimeMillis() + "/";
        File file = new File(path + fileName + ".xlsx");

        File dir = new File(path);
        if(!dir.exists()){
            dir.mkdirs();
        }

        FileInputStream in = null;
        OutputStream out = null;

        int count = baseService.selectCount(params);
        int queryPageSize = 200000;
        int fileMaxSize = 200000;
        int pageCount = count / queryPageSize;
        pageCount += (count > pageCount * queryPageSize ? 1 : 0);

        int fileCount = count / fileMaxSize;
        fileCount += (count > fileCount * fileMaxSize ? 1 : 0);

        LinkedBlockingQueue<T> queue = new LinkedBlockingQueue<>(50000);
        List<Future<?>> futures = new ArrayList<>();

        AtomicBoolean hasException = new AtomicBoolean(false);

        params.put("pageSize",queryPageSize);

        for(int pageNum=1;pageNum <= pageCount;pageNum++){
            int startRowNum = (pageNum - 1) * queryPageSize;
            params.put("startRowNum",startRowNum);
            Future<Boolean> submit = executors.submit(new GetTask<T>(params, queue,hasException));
            futures.add(submit);
        }

        List<SaveTask> saveTaskList = new ArrayList<>();
        List<File> files = new ArrayList<>();
        WriteSheet sheet = null;
        ExcelWriter writer = null;
        ExcelWriterBuilder writerBuilder = null;
        ExcelExportComplex exportComplex = null;
        for(int i=1;i<=fileCount;i++){
            file = new File(path + fileName + (i==1?"":i) + ".xlsx");
            files.add(file);
            writerBuilder = EasyExcel
                    .write(file, getModelClass())
                    .registerWriteHandler(getCommentWriterHandler());
            if (getCommentWriterHandler() != null) {
                writerBuilder = writerBuilder.inMemory(Boolean.TRUE);
            }

            int fileSize = i==fileCount ? (count-(fileCount-1)*fileMaxSize):fileMaxSize;
            exportComplex = new ExcelExportComplex(fileName,getSheetName(),writerBuilder,fileSize);
            SaveTask saveTask = new SaveTask(queue, exportComplex,hasException);
            Future<?> submit = executors.submit(saveTask);
            futures.add(submit);
        }



        try {
            for(int index = 0;index < futures.size();index ++){
                futures.get(index).get();
            }

            for(SaveTask saveTask:saveTaskList){
                saveTask.isEnd = true;
            }

            if(hasException.get()){
                throw new ServiceException("导出异常");
            }
            //把几个xlsx文件压缩到一个zip包里
            File zipFile = getZipFile(files,fileName,path);

            response.reset();
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/x-msdownload");
            response.setHeader("Content-Disposition", "attachment;filename=" + zipFile.getName());

            in = new FileInputStream(zipFile);
            out = response.getOutputStream();
            byte[] buf = new byte[4089];
            int len;
            while ((len = in.read(buf)) > 0) {
                out.write(buf, 0, len);
            }

        } catch (Exception e) {
            log.error("导出异常", e);
            hasException.getAndSet(true);
            throw e;
        } finally {
            if (null != in) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (null != out) {
                try {
                    out.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            file.delete();
        }
        Date endDate = new Date();
        System.out.println("耗时："+(endDate.getTime()-startDate.getTime())+"ms");
    }

    public File getZipFile(List<File> files, String fileName,String path) throws Exception {
        ZipOutputStream out = null;
        FileInputStream in = null;
        File zipfile = new File(path + fileName + ".zip");
        try {
            out = new ZipOutputStream(new FileOutputStream(zipfile));
            byte[] buf = new byte[4096];

            for(File file: files){
                try {
                    in = new FileInputStream(file);
                    out.putNextEntry(new ZipEntry(file.getName()));
                    int len;
                    while( (len = in.read(buf)) > 0){
                        out.write(buf, 0, len);
                    }
                    out.closeEntry();
                }catch (Exception e) {
                    log.info("文件压缩流导入异常",e);
                    if(out != null){
                        out.close();
                    }
                    if(zipfile != null){
                        zipfile.delete();
                    }
                    if(zipfile.getParentFile() != null){
                        zipfile.getParentFile().delete();
                    }
                    throw e;
                } finally {
                    if(in != null){
                        in.close();
                    }
                    if(file != null){
                        file.delete();
                    }
                }
            }
        } catch (IOException ex){
            log.info("文件压缩流导入异常",ex);
            throw ex;
        } finally {
            if(out != null){
                out.close();
            }
        }
        return zipfile;
    }
}



